/*
 Copyright (c) Whitemice Consulting, 2007, 2008, 2009
 Adam Tauno Williams <awilliam@whitemice.org>
 License: MIT/X11

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
*/
using System;
using System.Net;
using System.Linq;
using System.Text;
//using GLib;
using CookComputing.XmlRpc;
using System.Collections;
using System.Collections.Generic;
using Whitemice.ZOGI.RPC2;
using Whitemice.ZOGI.Backend;

namespace Whitemice.ZOGI
{
  public partial class Server
  { 
    internal  IzOGIProxy                  proxy;
    protected XmlRpcClientProtocol        client;
    protected bool                        established;
    protected IDictionary<string,int>    loginToObjectIdCache = null;
	  
	  protected void Establish(string hostname, int port, 
	                           string username, string password)
		{
			XmlRpcStruct ao = null;
			SetUserId(0);
		
      proxy = (IzOGIProxy)XmlRpcProxyGen.Create(typeof(Whitemice.ZOGI.RPC2.IzOGIProxy));
			client = (XmlRpcClientProtocol)proxy;
			client.ProtocolVersion = HttpVersion.Version10;
			client.PreAuthenticate = true;
			client.Timeout = 1800000;				
			client.Credentials = new NetworkCredential(username, password);
			client.Url = string.Format("http://{0}:{1}/zidestore/so/{2}/", hostname, port, username);
			try 
			{
        // TODO: We need the user's teams!
				ao = proxy.GetLoginAccount(65535);
				SetUserId((int)ao["objectId"]);
        if (ao.ContainsKey("_MEMBERSHIP"))
        {
          var teamList = (ao["_MEMBERSHIP"] as object[]);
          teamIds = new int[teamList.Length];
          for(int i = 0; i <  teamIds.Length; i++)
          {
            var team = (teamList[i] as XmlRpcStruct);
            teamIds[i] = Convert.ToInt32(team["targetObjectId"]); 
          }
        } else this.ZLOG("Account object from server contains no _MEMBERSHIP key");
        UserProfile.Initialize();
			} catch (XmlRpcFaultException ex) 
					{ OnException( ex, ex.FaultCode, ex.FaultString); }
				catch (Exception ex) 
				  { OnException(ex, 0, "N/A"); }		  
			if (UserId == 0)
			{
			  // Failure to acquite account Id
			  client = null;
			  proxy = null; 
			  return;
			}
			// Connection assumed to be successful
      PreloadCache();
			if (this.OnConnected != null) 
			  OnConnected(this, null);
		} // end Establish

    protected void PreloadCache()
    {
      IZOGICommand  command;

      //TODO: Request empty teams and do a search for accounts
      command = new TeamSearchCommand(RpcPriority.Immediate, null, 256, "teams");
      Server.Connection.RunCommand(command);
    }

    protected int[] teamIds;
    public int[] TeamIds { get { return teamIds; } }
    protected void SetTeamIds(int[] ids) { teamIds = ids; }
    
		protected int _userId;
		protected void SetUserId(int id) { _userId = id; }
		public int UserId { get { return _userId; } }

    /// <summary>
    /// Get the contact entity with the specified login, every contact entity
    /// a unique login (unique constraint in the backend database)
    /// </summary>
    /// <param name="login">
    /// A <see cref="System.String"/>
    /// </param>
    /// <returns>
    /// A <see cref="Whitemice.ZOGI.Contact"/>
    /// </returns>
    public Contact GetForLogin(string login)
    {
      object result = null;
      IZOGICommand  command = null;
      //TODO: Can we cache this?  Some utilities, like LDAPTeamSync, do A LOT of
      //lookups by login (since they start by looking at user names in a POSIX
      //group.  A login <-> objectId cache might be very effective.
      if (loginToObjectIdCache == null)
        loginToObjectIdCache = new Dictionary<string,int>();
      if (loginToObjectIdCache.ContainsKey(login))
      {
        var fetch = new FetchCommand(loginToObjectIdCache[login], 0, null);
        result = proxy.GetObjects(fetch.ObjectIds.ToArray<int>(), 0);
        command = fetch;
      } else
        {
          var search = new ContactSearchCommand(null);
          search.AddANDCriteria("login", login, false, false);
          //search.AddANDCriteria("isArchived", false, false, false);
          result = proxy.SearchForObjects("Contact", search.SearchCriteria, 0);
          command = search;
        }
      if (result is Array)
      {
        IEnumerable<Entity> list;
        Factory factory = new Factory(command);
        lock(Server.Cache)
        {
          list = factory.ResultsFromArray(result as Array);
        }
        if (list.Count() == 1)
        {
          var contact = (list.ElementAt(0) as Contact);
          if (loginToObjectIdCache.ContainsKey(login))
            loginToObjectIdCache[login] = contact.ObjectId;
          else
            loginToObjectIdCache.Add(login, contact.ObjectId);
          return contact;
        }
      }
      return null;
    } // end GetForLogin

    /// <summary>
    /// Perform a syncronous request for the specified object at the
    /// specified detail level.
    /// </summary>
    /// <param name="objectId">
    /// A <see cref="System.Int32"/>
    /// </param>
    /// <param name="detailLevel">
    /// A <see cref="System.Int32"/>
    /// </param>
    /// <param name="useCache">
    /// A <see cref="System.Boolean"/>
    /// </param>
    /// <returns>
    /// A <see cref="Whitemice.ZOGI.Entity"/>, or null if the object is not
    /// available; it may be unavailable due to permissions or because the
    /// specified objectId does not exist.
    /// </returns>
    public Entity Get(int objectId, int detailLevel, bool useCache)
    {
      Entity        entity = null;
      Factory       factory;
      FetchCommand  command;

      lock(Server.Cache)
      {
        if (useCache)
        {
          if (Cache.Has(objectId, detailLevel, out entity))
            return entity;
        }
        int[] objectIds = { objectId };
        object result = proxy.GetObject(objectId, detailLevel);
        if (result is XmlRpcStruct)
        {
          command = new FetchCommand(objectIds, RpcPriority.Application, null, detailLevel, null);
          factory = new Factory(command);          
          entity = factory.ResultFromStruct(result as XmlRpcStruct);
        }
      }
      return entity;
    } // end Get

    /// <summary>
    /// Perform a syncronous put on the specified object.  If the objectId of
    /// the entity is zero a new entity is created, post creation the entity
    /// will have a non-zero objectId.
    /// </summary>
    /// <param name="entity">
    /// A <see cref="Whitemice.ZOGI.Entity"/>
    /// </param>
    public void Put(Entity entity)
    {
      object      response;
      Factory      factory;
      PutCommand   command;

      if (entity is IZOGIStorable)
      {
        response = proxy.PutObject((entity as IZOGIStorable).Encode());
        if (response is XmlRpcStruct)
        {
          lock(Server.Cache)
          {      
            command = new PutCommand(entity, null);
            factory = new Factory(command);
            factory.ResultFromStruct(response as XmlRpcStruct);
          }
        }
        factory = null;
        command = null;
      }
    } // end Put

    public bool Delete(Entity entity)
    {
      // TODO: This operation should user a DeleteCommand object
      this.ZLOG("Delete ObjectId#{0}", entity.ObjectId);
      if (entity is IZOGIStorable)
      {
        try
        {
          if (proxy.DeleteObject(entity.ObjectId, new string[] {}))
          {
            lock(Server.Cache)
            {
              Server.Cache.Delete(entity);
              Registry.Notify(entity,
                              new ServerEvent(entity,
                                              ServerEventType.Delete));
              entity = null;
            }
            return true;
          }
        } catch (Exception ex)
          {
            if (this.OnException != null)
              OnException(ex, 0, null);
            else throw ex;
          }
      }
      return false;
    } // end Delete
    
	  private void Issue(IZOGICommand c)
	  {
      this.ZLOG(string.Format("Issuing {0} {1}", c, c.Id));
	    if (c is IZOGIFetchCommand)
      {
        this.ZLOG("Fetch");
        IssueFetch(c as IZOGIFetchCommand);
      } else if (c is IZOGISearchCommand)
        {
          this.ZLOG("Search");
          IssueSearch(c as IZOGISearchCommand);
        } else if (c is IZOGIPutCommand)
          {
            this.ZLOG("Put");
            IssuePut(c as IZOGIPutCommand);
          } else if (c is IZOGIDeleteCommand)
            {
              this.ZLOG("Delete");
              IssueDelete(c as IZOGIDeleteCommand);
            }
	  } // end Issue
	  
	  private void IssueFetch(IZOGIFetchCommand c)
	  {
      if (c.UseCache)
      {
        if (SaitFetch(c))
        {
          this.ZLOG("Fetch saited from cache (late)");
          workQueue.Complete();
          return;
        }
      }
	    int[] objectIds = new int[c.ObjectIds.Count];
      StringBuilder s = new StringBuilder();
      s.Append("Issuing fetch for objects:");
      for(int i = 0; i < objectIds.Length; i++)
      {
        s.AppendFormat(" {0},", c.ObjectIds[i]);
	      objectIds[i] = c.ObjectIds[i];
      }
      this.ZLOG(s.ToString());
      s = null;
	    FetchObjects(objectIds, c.DetailLevel, c);
	  } // end IssueFetch
	  
	  private void IssueSearch(IZOGISearchCommand c)
	  {
	    SearchObjects(c.EntityName, c.SearchCriteria, c.DetailLevel, c);
	  } // end IssueFetch	  
	  
	  private void IssuePut(IZOGIPutCommand c)
	  {
      var entity = c.Entity;
	    if (entity is IZOGIStorable)
	    {
        this.ZLOG("**PUT ISSUED***");
	      var rpcStruct = (c.Entity as IZOGIStorable).Encode();
	      if (c.Flags != null)
	        rpcStruct.Add("_FLAGS", c.Flags);
	      PutObject(rpcStruct, c);
	    } else this.ZLOG(string.Format("Attempt to store a non-storable entity. entity is {0}", entity.EntityName));
	  } // end Issue Put
	  
	  private void IssueDelete(IZOGIDeleteCommand c)
	  {
	    DeleteObject(c.Entity.ObjectId, c.Flags, c);
	  } // end IssueDelete
	  
	  private void FetchObjects(int[] objectIds, 
	                            int detailLevel, 
	                            object state)
	  {
      	/*
      	string msg = "ISSUING RPC TO FETCH:";
      	foreach (int i in objectIds)
        	msg = string.Format("{0} {1}", msg, i.ToString());
	  	Console.WriteLine(msg);
        */
	    proxy.BeginGetObjects(objectIds, 
	                          detailLevel, 
	                          new AsyncCallback(this.Callback),
	                          state);
	  } // end FetchObjects
	  
	  private void SearchObjects(string entityName,
	                             object criteria,
	                             int detailLevel,
	                             object state)
	  {
	    proxy.BeginSearchForObjects(entityName,
	                                criteria,
	                                detailLevel,
	                                new AsyncCallback(this.Callback),
	                                state);
	  } // end SearchObjects
	  
	  private void PutObject(XmlRpcStruct entity,
	                         object state)
	  {
      this.ZLOG("***PUT OBJECT***");
	    proxy.BeginPutObject(entity,
	                         new AsyncCallback(this.Callback),
	                         state);
	  } // end PutObject
	  
	  private void DeleteObject(int objectId,
	                            string[] flags,
	                            object state)
	  {
      this.ZLOG("Delete ObjectId#{0}", objectId);
      if (flags == null) flags = new string[] {};
	    proxy.BeginDeleteObject(objectId, 
	                            flags,
	                            new AsyncCallback(this.Callback), 
	                            state);
	  } // end DeleteObject
	  
	  internal void Callback(IAsyncResult asyncResult)
	  {
      var context = asyncResult.AsyncState;
      if (context is IZOGICommand)
      {
        // Commands issued with a priority of Application have their responses processed
        // with a priority of Immediate.  All other priorities are processed unchanged.
        IZOGICommand z = (context as IZOGICommand);
        if (z.Priority == RpcPriority.Application)
        {
          workQueue.Enqueue(asyncResult, RpcPriority.Immediate);
        } else
          {
            workQueue.Enqueue(asyncResult, z.Priority);
          }
        return;
      } else
        {
          // TODO: Is it possible to get a callback not issued from a command?
	        workQueue.Enqueue(asyncResult, RpcPriority.Immediate);
        }
	  } // end Callback
	  
	  protected void ProcessResult(IAsyncResult asyncResult)
	  {
	    IZOGICommand                  command;
	    object                        result;
	    Factory                       factory;

	    command = (asyncResult.AsyncState as IZOGICommand);
      this.ZLOG("Received callback for command {0}", command.Id);      
	    factory = new Factory(command);
	    result = null;
	    
	    if (command is Whitemice.ZOGI.IZOGIFetchCommand)
	    {
        try
        {
	        result = proxy.EndGetObjects(asyncResult);
          MultiResultCallback(result, factory);
        } catch (System.Net.WebException exception)
          {
            this.ZLOG(exception.Message);
            this.ZLOG(exception.Message);
            this.ZLOG("Submitting command {0} back to the queue.", command.Id);
            workQueue.Enqueue(command, command.Priority);
          }
          catch (CookComputing.XmlRpc.XmlRpcIllFormedXmlException exception)
          {
            this.ZLOG(exception.Source);
            this.ZLOG(exception.StackTrace);
            MultiResultCallback(null, factory);
          }
	      return;
	    }
	    
	    if (command is Whitemice.ZOGI.IZOGISearchCommand)
	    {
        try
        {
          result = proxy.EndSearchForObjects(asyncResult);
          MultiResultCallback(result, factory);
        } catch (System.Net.WebException exception)
          {
            this.ZLOG(exception.Message);
            this.ZLOG(exception.Message);
            this.ZLOG("Submitting command {0} back to the queue.", command.Id);
            workQueue.Enqueue(command, command.Priority);
          }
	      return;
      }
      	      
	    if (command is IZOGIPutCommand)
	    {
	      result = proxy.EndPutObject(asyncResult);
	      PutCallback(result, factory);
	      return;
	    }
	    
	    if (command is IZOGIDeleteCommand)
	    {
        this.ZLOG("IZOGIDeleteCOmmand result");
	      result = proxy.EndDeleteObject(asyncResult);
	      DeleteCallback(result, factory);
	      return;
	    }

      throw new Exception("Unhandled command callback");
    } // end Callback
    
    private void MultiResultCallback(object result, Factory factory)
    {
      IEnumerable<Whitemice.ZOGI.Entity>  list;

      this.ZLOG("MultiResultCallback");
      try
      {
    	  if (result is Array)
	      {
          this.ZLOG("Waiting for cache lock.");
	        lock(Server.Cache)
	        {
            this.ZLOG("Cache lock acquired.");
           	list = factory.ResultsFromArray(result as Array);
              //Console.WriteLine("Factory returned {0} objects.", list.Count());
	        }
          this.ZLOG("Cache lock released.");
          if (factory.Command.Callback != null)
          {
            RPCEventArgs args;
            try
            {
              args = new RPCEventArgs(factory.Command.Id,
                                      list,
                                      factory.Command.Context,
                                      null);
              this.ZLOG(string.Format("Calling back to application with {0} entities", list.Count()));
              DateTime stamp = DateTime.Now;
	            factory.Command.Callback(this, args);
              int ms = DateTime.Now.Subtract(stamp).Milliseconds;
              this.ZLOG(string.Format("Lost {0}ms in callback", ms));
              return;
            } catch(Exception exception)
              {
                Console.WriteLine("Exception invoking callback.");
                Console.WriteLine(exception.Message);
                Console.WriteLine(exception.StackTrace);
                Console.WriteLine("ObjectIDs:");
                foreach(Entity e in list) Console.Write(" {0}", e.ObjectId);
                Console.WriteLine();
              }                       
          }
	      } else 
          {
            if (result == null)
              this.ZLOG("RPC result is null!");
            else
              this.ZLOG(string.Format("RPC result of unexpected type, result is {0}", result));
            throw new Exception(string.Format("RPC Result of unexpected type {0}", result));
          }
      } catch (Exception exception)
        {
          if (factory.Command.Callback != null)
            factory.Command.Callback(this,
                                     new RPCEventArgs(factory.Command.Id,
                                                      null,
                                                      factory.Command.Context,
                                                      exception));
        }
        finally
        {
          DateTime stamp = DateTime.Now;
          workQueue.Complete();
          int ms = DateTime.Now.Subtract(stamp).Milliseconds;
          this.ZLOG(string.Format("Lost {0}ms in workQueue.Complete", ms));
        }
	  } // end MultiResultCallback

	  private void PutCallback(object result, Factory factory)
	  {
      Entity  entity;

      try
      {
        this.ZLOG("PutCallback");
        lock(Server.Cache)
        {
          entity = factory.ResultFromStruct(result as XmlRpcStruct);
        }
        if (factory.Command.Callback != null)
          factory.Command.Callback(this,
                                   new RPCEventArgs(factory.Command.Id,
                                                    new Entity[] { entity },
                                                    factory.Command.Context,
                                                    null));
      } catch (Exception exception)
        {
          this.ZLOG(string.Format("PutCallback Exception: {0}", exception.Message));
          if (factory.Command.Callback != null)
            factory.Command.Callback(this,
                                     new RPCEventArgs(factory.Command.Id,
                                                      null,
                                                      factory.Command.Context,
                                                      exception));
        }
        finally
        {
          this.ZLOG("PutCallback.Finally");
          workQueue.Complete();
        }
	  } // end PutCallback
	  
    private void DeleteCallback(object result, Factory factory)	  
    {
      bool  response;
      
      this.ZLOG("DeleteCallback");
      try
      {
        var command = (factory.Command as DeleteCommand);
        lock(Server.Cache)
        {
          try
          {
            response = (bool)result;
          } catch (Exception ex)
            {
              this.ZLOG("Deletion response is not a boolean value.");
              response = false;
            }
          if (response)
            Server.Cache.Delete(command.Entity);
          else throw new Exception("Deletion returned failure.");
        }
        Registry.Notify(command.Entity,
                        new ServerEvent(command.Entity,
                                        ServerEventType.Delete));
        if (factory.Command.Callback == null)
        {
          this.ZLOG("DeleteCommand has no callback");
        } else
          {
            // do the callback, delete callback does not provide an entity, the
            // receivers server event listener should have already cleaned up
            // the contents,  callback is primarily to finalize the event.
            factory.Command.Callback(this,
                                     new RPCEventArgs(command.Id,
                                                      null,
                                                      command.Context,
                                                      null));
          }
      } catch (Exception ex)
        {
          if (factory.Command.Callback == null)
          {
            this.ZLOG("DeleteCommand has no callback");
            if (OnException == null)
              this.ZLOG("No receiver for DeleteCommand exception.");
              else this.OnException(ex, 0, null);
          } else
            {
              // send exception in command callback
              factory.Command.Callback(this,
                                       new RPCEventArgs(factory.Command.Id,
                                                        null,
                                                        factory.Command.Context,
                                                        ex));
            }
        } finally
          {
            this.ZLOG("DeleteCallback.Finally");
            workQueue.Complete();
          }
    }
  } // end class
}
